﻿using System;
using System.Collections.Generic;
using System.Linq;
using TinyContainer.Attributes;


namespace TinyContainer
{
    /// <summary>
    /// The default implementation of the <c>IContainer</c> interface
    /// providing basic dependency injection functionality.
    /// </summary>
    /// <remarks>
    /// Currently, the <b>Container</b> supports constructor injection.
    /// Other techniques for injection are coming soon...
    /// </remarks>
    public class Container : IContainer
    {
        private MappingCollection _mapping;


        /// <summary>
        /// Initializes a new instance of the <see cref="TinyContainer.Container"/> class.
        /// </summary>
        public Container()
        {
            this._mapping = new MappingCollection();
            this.Register<IContainer>(this);
        }


        /// <summary>
        /// Attempts to resolve(initialize) the specified object type.
        /// </summary>
        /// <param name="interface">The type of the object.</param>
        /// <param name="name">The name of the object.</param>
        /// <returns>An instance of the specified object type.</returns>
        public object Resolve(Type @interface, string name = "")
        {
            var instance = this.FindInstanceOfType(@interface, name);
            if (instance != null) return instance;

            var type = this.FindType(@interface, name);
            if (type == null) throw new Exception("The type " + @interface.ToString() + " cannot be resolved");

            var obj = ConstructObject(type);
            if (obj == null) throw new Exception("Cannot resolve " + @interface.ToString());

            this._mapping[@interface, name].Instance = obj;

            return obj;
        }

        /// <summary>
        /// Attempts to resolve(initialize) the specified object type.        
        /// </summary>
        /// <typeparam name="T">The type of the object.</typeparam>
        /// <param name="name">The name of the object.</param>
        /// <returns>An instance of the specified object type.</returns>
        public T Resolve<T>(string name = "")
        {
            return (T)Resolve(typeof(T), name);
        }

        /// <summary>
        /// Attempts to construct the object by calling the object's constructor.
        /// </summary>
        /// <param name="type">The type of the object.</param>
        /// <returns>An instance of the object.</returns>
        private object ConstructObject(Type type)
        {
            var constructor = type.GetConstructors().FirstOrDefault();
            if (constructor != null)
            {
                var constparams = new List<object>();
                foreach (var param in constructor.GetParameters())
                {
                    constparams.Add(this.Resolve(param.ParameterType));
                }
                var obj = constructor.Invoke(constparams.ToArray());

                //now check for export params and do the imports
                foreach (var prop in type.GetProperties())
                {
                    var attr = prop.GetCustomAttributes(typeof(ImportAttribute), false).FirstOrDefault() as ImportAttribute;
                    if (attr != null)
                    {
                        var inject = this.Resolve(prop.PropertyType, attr.Name);
                        prop.SetValue(obj, inject, null);
                    }
                }
                return obj;
            }
            else
            {
                throw new Exception("There is no constructor for type " + type.ToString());
            }
        }


        /// <summary>
        /// Searches for the specified object <c>Object.Type</c>.
        /// </summary>
        /// <remarks>
        /// If the <c>Object.Type</c> is an interface, it returns an interface.
        /// If the <c>Object.Type</c> is a class, it returns a class.
        /// </remarks>
        /// <param name="interface">The type of the object.</param>
        /// <param name="name">The name of the object.</param>
        /// <returns></returns>
        private Type FindType(Type @interface, string name)
        {
            Type type = null;
            if (@interface.IsInterface)
            {
                return this._mapping[@interface, name].Class;
            }
            else
            {
                type = @interface;
            }
            return type;
        }


        /// <summary>
        /// Searches for an instance of the specified <c>Object.Type</c>.
        /// </summary>
        /// <param name="type">The type of the object.</param>
        /// <param name="name">The name of the object.</param>
        /// <returns>An instance of the object.</returns>
        private object FindInstanceOfType(Type type, string name)
        {
            return (from val in this._mapping where type.IsInstanceOfType(val.Instance) && val.Name == name select val.Instance).FirstOrDefault();
        }


        /// <summary>
        /// Registers the specified object.
        /// </summary>
        /// <typeparam name="T">The <c>Object.Type</c> of the object.</typeparam>
        /// <typeparam name="C">The Class implementing the <c>Object.Type</c> of the object.</typeparam>
        /// <param name="name">The name of the object.</param>
        public void Register<T, C>(string name = "") where C : T
        {
            var @interface = typeof(T);
            var @class = typeof(C);

            var map = this._mapping[@interface, name];
            map.Class = @class;
            map.Instance = null;                
        }


        /// <summary>
        /// Checks whether the specified object is registered.
        /// </summary>
        /// <typeparam name="T">The <c>Object.Type</c> of the object.</typeparam>
        /// <param name="name">The name of the object.</param>
        /// <returns>
        ///   <c>True</c> if the specified object is registered; otherwise, <c>False</c>.
        /// </returns>
        public bool IsRegistered<T>(string name = "")
        {
            var type = typeof(T);
            return this._mapping.IsRegistered(type, name);
        }


        /// <summary>
        /// Overloaded. Registers an object instance.
        /// </summary>
        /// <typeparam name="T">The type of the object.</typeparam>
        /// <param name="instance">The type of the object instance.</param>
        /// <param name="name">The name of the object.</param>
        public void Register<T>(T instance, string name = "")
        {
            var @interface = typeof(T);
            this._mapping[@interface, name].Instance = instance;
        }


        /// <summary>
        /// Deletes the specified object instance from the container registry.
        /// </summary>
        /// <param name="instance">The name of the object instance.</param>
        public void Remove(object instance)
        {
            this._mapping.RemoveInstance(instance);
        }
    }
}
